package org.core;

import org.constant.Constant;
import org.util.FileUtils;
import org.util.HttpUtils;
import org.util.LogUtils;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.RandomAccess;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @Auther: qingle
 * @Date: 2024/9/8-6:34
 * @Description: 下载器类
 * <p>
 * 从硬件层面简述如下：
 * 从网络接口接收HTTP响应数据，并将其作为输入流 inputStream。
 * 使用缓冲区在内存中暂存输入数据，减少对较慢的网络接口的访问频率，提高读取效率。
 * 将数据从内存中的缓冲区写入到存储设备上的目标文件 httpFileName。
 * 使用缓冲区在内存中暂存待写入的数据，减少对较慢的存储设备的访问频率，提高写入效率。
 * @version: 1.0
 */
public class DownLoader {

	public ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

	// 线程池对象
	public ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(Constant.THREAD_NUM, Constant.THREAD_NUM, 0L,
		TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(5));

	private CountDownLatch countDownLatch = new CountDownLatch(Constant.THREAD_NUM);

	public void download(String url) throws IOException, InterruptedException {
		// 获取文件名
		String httpFileName = HttpUtils.getHttpFileName(url);
		// 文件下载路径
		httpFileName = Constant.PATH + httpFileName;

		DownloadInfoThread downloadInfoThread = null;

		// 获取本地文件大小
		long localFileSize = FileUtils.getLocalFileSize(httpFileName);

		// 获取链接对象
		HttpURLConnection httpURLConnection = null;

		try {
			httpURLConnection = HttpUtils.getHttpURLConnection(url);
			int contentLength = httpURLConnection.getContentLength();

			// 如果本地文件大小大于等于网络文件大小，则无需下载
			if (localFileSize >= contentLength) {
				LogUtils.info("{}文件大小大于网络文件大小，无需重复下载", httpFileName);
				return;
			}

			// 创建获取信息的对象
			downloadInfoThread = new DownloadInfoThread(contentLength);
			// 将任务交给线程执行，每隔一秒下载一次
			scheduledExecutorService.scheduleAtFixedRate(downloadInfoThread, 1, 1, java.util.concurrent.TimeUnit.SECONDS);


			// 切分任务
			ArrayList<Future> futures = new ArrayList<>();
			splits(url, futures);

//			futures.forEach(future -> {
//				try {
//					future.get(); // 方便后续的合并操作
//				} catch (InterruptedException | ExecutionException e) {
//					e.printStackTrace();
//				}
//			});

			countDownLatch.await();

			// 合并文件
			if (merge(httpFileName)) {
				// 删除临时文件
				deleteTempFile(httpFileName);
			}

			LogUtils.info("文件大小：{}", contentLength);
		} catch (IOException e) {
			e.printStackTrace();
		}

		try (
			// 从HTTP连接获取输入流
			InputStream inputStream = httpURLConnection.getInputStream();

			// 包装输入流为带缓冲区的输入流，以提高读取效率
			BufferedInputStream bis = new BufferedInputStream(inputStream);

			// 创建文件输出流，用于将数据写入到目标文件中
			FileOutputStream fos = new FileOutputStream(httpFileName);

			// 包装文件输出流为带缓冲区的输出流，以提高写入效率
			BufferedOutputStream bos = new BufferedOutputStream(fos)
		)
		/**
		 * 从输入流读取数据并写入输出流
		 * 此代码段实现了将数据从输入流传输到输出流的功能
		 * 它通过循环读取输入流中的字节，并将它们写入输出流，直到输入流的末尾
		 */ {
			// 初始化读取的字节长度，初始值为-1，用于在循环中作为条件判断
			int len = -1;
			// 循环读取输入流中的字节，直到达到输入流的末尾
			byte[] buffer = new byte[Constant.BYTE_SIZE];

			while ((len = bis.read(buffer)) != -1) {
				DownloadInfoThread.downSize.doubleValue() += len;
				// 将读取的字节写入输出流
				bos.write(len);
			}
		} catch (FileNotFoundException e) {
			LogUtils.error("下载文件不存在{}", url);
		} catch (Exception e) {
			LogUtils.error("下载失败", e);

		} finally {
			System.out.println("\r");
			System.out.println("下载完成");
			// 关闭连接和流
			if (httpURLConnection != null) {
				httpURLConnection.disconnect();
			}

			// 关闭
			scheduledExecutorService.shutdown();

			// 关闭线程池
			poolExecutor.shutdown();

		}


	}


	// 分块代码

	/**
	 * 文件切分
	 * @param url
	 * @param futureList
	 */

	public void splits(String url, ArrayList<Future> futureList) {
		// 获取文件大小
		try {
			long httpFileContentLength = HttpUtils.getHttpFileContentLength(url);

			// 计算每个线程下载的大小
			long size = httpFileContentLength / Constant.THREAD_NUM;

			// 计算分块个数
			for (int i = 0; i < Constant.THREAD_NUM; i++) {
				// 计算下载起始位置
				long startPos = i * size;
				long endPos;
				if(i == Constant.THREAD_NUM - 1) {
					endPos = 0;
				} else {
					endPos = startPos + size;
				}

				// 如果不是第一块，起始位置要加1；
				if(startPos != 0) {
					startPos += 1;
				}

				// 创建下载任务
				DownloaderTask downloaderTask = new DownloaderTask(url, startPos, endPos, i, countDownLatch);

				// 提交任务到线程池中
				Future<Boolean> future = poolExecutor.submit(downloaderTask);

				// 将任务添加到列表中
				futureList.add(future);

			}

			LogUtils.info("每个线程下载的大小为：{}", size);

			// 创建获取下载信息的任务对象
			DownloadInfoThread downloadInfoThread = new DownloadInfoThread(httpFileContentLength);
			scheduledExecutorService.scheduleAtFixedRate(downloadInfoThread, 1, 1, TimeUnit.SECONDS);


		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	/**
	 * 文件合并
	 * @param fileName
	 * @return
	 */
	public boolean merge(String fileName) {
		LogUtils.info("开始合并文件{}", fileName);
		byte[] buffer = new byte[Constant.BYTE_SIZE];
		int len = -1;
		try (RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw")){

			for (int i = 0; i < Constant.THREAD_NUM; i++) {
				try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName + ".temp" + i))) {
					while((len = bis.read(buffer)) != -1) {
						accessFile.write(buffer, 0, len);
					}
				}
			}
			LogUtils.info("合并完成");

		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}

	public boolean deleteTempFile(String fileName) {
		for (int i = 0; i < Constant.THREAD_NUM; i++) {
			File file = new File(fileName + ".temp" + i);
			file.delete();
			LogUtils.info("删除临时文件{}", file.getName());
		}
		return true;
	}

}
